#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <termios.h>

#define MAX_DATA 512 
#define MAX_ROWS 100
#define HELP "readme.txt"
#define DBFILE "db.dat"
#define DEL() printf("\033[D \033[D")

typedef enum {
    false,
    true
} bool;

struct Address {
    int id;
    int set;
    char name[MAX_DATA];
    char email[MAX_DATA];
};

struct Database {
    struct Address rows[MAX_ROWS];
};

struct Connection {
    FILE *file;
    struct Database *db;
};

struct DataNode {
    char *cmd;
    char *desc;
    int (*handler)(struct Connection *conn);
    struct DataNode *next;
};

typedef struct DataNode MenuNode;

bool login(int N);
int quit(struct Connection *conn);
int showhelp(struct Connection *conn);
void cyscanf(char *str, int n, bool show);
void die(const char *message, struct Connection *conn);
MenuNode *FindCmd(MenuNode *node, const char *cmd);

struct Connection *Database_open();
void Database_init(struct Connection *conn);
void Database_close(struct Connection *conn);
int Database_write(struct Connection *conn);

void Address_show(struct Address *addr);
void Address_get(struct Connection *conn, int id);
void Address_insert(struct Connection *conn, int id, const char *name, const char *email);
int Address_delete(struct Connection *conn);
int Address_print(struct Connection *conn);
int Address_add(struct Connection *conn);
int Address_list(struct Connection *conn);
void encode(char *str);
void decode(char *str);

static MenuNode menulist[] = {
    {.cmd = "help", .desc = "show all command", .handler = showhelp, .next = &menulist[1]},
    {.cmd = "w", .desc = "save data to file", .handler = Database_write, .next = &menulist[2]},
    {.cmd = "d", .desc = "delete record", .handler = Address_delete, .next = &menulist[3]},
    {.cmd = "g", .desc = "print address info", .handler = Address_print, .next = &menulist[4]},
    {.cmd = "l", .desc = "show all address info", .handler = Address_list, .next = &menulist[5]},
    {.cmd = "i", .desc = "insert a new address", .handler = Address_add, .next = &menulist[6]},
    {.cmd = "q", .desc = "quit", .handler = quit, .next = &menulist[7]},
    {.cmd = "v", .desc = "v1.0", .handler = NULL, .next = NULL},
};

bool login(int N)
{
    char name[MAX_DATA];
    char pwd[MAX_DATA];
    int count = 0;
    while(count < N) {
        printf("username:");
        cyscanf(name, MAX_DATA, true);
        printf("password:");
        cyscanf(pwd, MAX_DATA, false);
        if(strcmp(name, "czx") == 0 && strcmp(pwd, "123") == 0)
            break;
        
        count++;
    }

    return count < N ? true : false;
}

void cyscanf(char *str, int n, bool show)
{
    int len = 0;
    char ch;
    struct termios old_term;
    struct termios cur_term;
    tcgetattr(STDIN_FILENO, &old_term);
    memcpy(&cur_term, &old_term, sizeof(cur_term));
    cur_term.c_lflag &= ~(ICANON|ECHO|ECHOE|ECHOK|ECHONL);
    cur_term.c_cc[VMIN] = 1;
    cur_term.c_cc[VTIME] = 0;
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &cur_term);

    while (1)
    {
        ch = getchar();
        if (ch == '\n') break;
        if (ch == 8 && len > 0)
        {
            // backspace
            len--;
            if(show) DEL();
            continue;
        }
        if (ch == '\33')
        {
            ch = getchar();
            if (ch == '[')
            {
                ch = getchar();
                switch (ch)
                {
                    case 'A':
                        //printf("\nMOVEUP");
                        break;
                    case 'B':
                        //printf("\nMOVEDOWN");
                        break;
                    case 'C':
                        //printf("\nMOVERIGHT");
                        break;
                    case 'D':
                        //printf("\nMOVELEFT");
                        break;
                }
                continue;
            }
        }
        if (ch > 31 && ch <127 && len < n)
        {
            if(show) printf("%c", ch);
            str[len++] = ch;
        }
        fflush(stdout);
    }
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_term);
    str[len] = '\0';
    printf("\n");
}

void encode(char *str)
{
    char ch;
    int i = 0;
    while((ch = str[i]) != '\0') {
        str[i] = str[i] + 5;
        i++;
    }
}

void decode(char *str)
{
    char ch;
    int i = 0;
    while((ch = str[i]) != '\0') { 
        str[i] = str[i] - 5;
        i++;
    }
}

void die(const char *message, struct Connection *conn)
{
    if(errno) {
        perror(message);
    } else {
        printf("ERROR: %s\n", message);
    }
	Database_close(conn);
    exit(1);
}

struct Connection *Database_open()
{
    struct Connection *conn = malloc(sizeof(struct Connection));
    if(!conn) die("Memory error, failed to create Connection", conn);

    conn->db = malloc(sizeof(struct Database));
    if(!conn->db) die("Memory error, failed to create Database", conn);

	conn->file = fopen(DBFILE, "r+");
	if(!conn->file)
	{
		conn->file = fopen(DBFILE, "w");
        if(!conn->file) die("Failed to open db file.", conn);
		Database_init(conn);
		Database_write(conn);
	} else {
	    int rc = fread(conn->db, sizeof(struct Database), 1, conn->file);
        if(rc != 1) die("Failed to load database.", conn);
    }
	
    return conn;
}

void Database_init(struct Connection *conn)
{
    int i = 0;

    for(i = 0; i < MAX_ROWS; i++) {
        // make a prototype to initialize it
        struct Address addr = {.id = i, .set = 0};
        // then just assign it
        conn->db->rows[i] = addr;
    }
}

int Database_write(struct Connection *conn)
{
    rewind(conn->file);
    int rc = fwrite(conn->db, sizeof(struct Database), 1, conn->file);
    if(rc != 1) printf("Failed to write database.\n");
    rc = fflush(conn->file);
    if(rc == -1) printf("Cannot flush database.\n"); 

    return 1;
}

void Database_close(struct Connection *conn)
{
    if(conn) {
        if(conn->file) fclose(conn->file);
        if(conn->db) free(conn->db);
        free(conn);
    }
}

int showhelp(struct Connection *conn)
{
	//MenuNode *p = menulist;
    //while(NULL != p)
    //{
    //    printf("%-10s - %s\n", p->cmd, p->desc);
    //    p = p->next;
    //}
    char ch;
    FILE *fp = fopen(HELP, "r");
    if(fp == NULL)
    {
        printf("failed to open help file.\n");
        return 1;
    }
    while((ch = fgetc(fp)) != EOF) 
        printf("%c", ch);
    
    fclose(fp);	
	return 1;
}

int quit(struct Connection *conn)
{
    return 0;
}

void Address_show(struct Address *addr)
{
    char email[MAX_DATA];
    strncpy(email, addr->email, MAX_DATA);
    decode(email);
	printf("%d %s %s\n",
        addr->id, addr->name, email);
}

void Address_get(struct Connection *conn, int id)
{
    if(id > MAX_ROWS - 1)
    {
        printf("id out of range.\n");
        return;
    }
    struct Address *addr = &conn->db->rows[id];

    if(addr->set) {
        Address_show(addr);
    } else {
        printf("ID is not set\n");
    }
}

int Address_print(struct Connection *conn)
{
	int id;
    printf("Input id: ");
    scanf("%d", &id);
    Address_get(conn, id);

    return 1;
}

void Address_insert(struct Connection *conn, int id, const char *name, const char *email)
{
    if(id > MAX_ROWS - 1)
    {
        printf("id out of range.\n");
        return;
    }
	struct Address *addr = &conn->db->rows[id];
	if(addr->set) 
    {
        printf("Already set, delete it first.\n");
        return;
    }

    addr->set = 1;
    // WARNING: lost somthing when the given name is greater than MAX_DATA
    char *res = strncpy(addr->name, name, MAX_DATA);
    // demonstrate the strncpy bug
    if(!res) die("Name copy failed", conn);

    res = strncpy(addr->email, email, MAX_DATA);
    if(!res) die("Email copy failed", conn);
    Database_write(conn);
}

int Address_add(struct Connection *conn)
{
    if(conn == NULL) return 1;
    
    char name[MAX_DATA];
    char email[MAX_DATA];
    int id;
    printf("Input id: ");
    scanf("%d", &id);
    getchar();//eat '\n'
    printf("Input name: ");
	cyscanf(name, MAX_DATA, true);
    printf("Input email: ");
	cyscanf(email, MAX_DATA, true);
    encode(email);
    Address_insert(conn, id, name, email);

    return 1;
}

int Address_delete(struct Connection *conn)
{
	int id;
    printf("Input id: ");
    scanf("%d", &id);
    getchar();
    if(id > MAX_ROWS - 1)
    {
        printf("id out of range.\n");
        return 1;
    }
    struct Address addr = {.id = id, .set = 0};
    conn->db->rows[id] = addr;
    Database_write(conn);
	return 1;
}

int Address_list(struct Connection *conn)
{
    int i = 0;
    struct Database *db = conn->db;

    for(i = 0; i < MAX_ROWS; i++) {
        struct Address *cur = &db->rows[i];

        if(cur->set) {
            Address_show(cur);
        }
    }
	
	return 1;
}

MenuNode *FindCmd(MenuNode *node, const char *cmd)
{
    if(NULL == cmd) return NULL;

    MenuNode *p = node;
    while(NULL != p)
    {
        if(0 == strcmp(cmd, p->cmd))
            break;

        p = p->next;
    }

    return p;
}

int main(int argc, char *argv[])
{
    if(login(3) == false)
    {
        printf("Wrong username or password.\n");
        return 0;
    }
    
    printf("cy> login success.\n");
	char cmd[50];
    int r = 1;
	struct Connection *conn = Database_open();
	while(r) {
        printf("cy> ");
        cyscanf(cmd, 50, true);
        MenuNode *p = FindCmd(menulist, cmd);
        if(NULL == p)
        {
            showhelp(conn);
            continue;
        }

        if(NULL != p->handler)
            r = p->handler(conn);
        else
            printf("%-10s - %s\n", cmd, p->desc);
    }

	Database_close(conn);

    return 0;
}
